-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.
-- Description:
--   This class is responsible for canbus collection, specified functions are following:
--   1. Management for all canbus instances
--   2. Functional interfaces of canbus collection, such as batch sending, or statistics

local log = require 'mc.logging'
local class = require 'mc.class'
local singleton = require 'mc.singleton'
local definition = require 'canbus.definition'
local object_mgmt = require 'objects.manager'
local canbus_dispatcher = require 'canbus.dispatcher'
local canbus_collection = require 'canbus.collection'
local receiver_instance = require 'tcp.receiver'
local tcp_mgmt_instance = require 'tcp.tcp_mgmt'
local consts = require 'constants'
local skynet = require 'skynet'
local vos = require 'utils.vos'
local json = require 'cjson'
local utils = require 'mc.utils'
local file_checker = require 'utils.file'
local file_sec = require 'utils.file'
local sender_instance = require 'tcp.sender'
local data_sync_instance = require 'objects.data_sync'

local data_mgmt = class()

local tmp_dir_path = '/dev/shm/bmc_datasync'

function data_mgmt:ctor()
    self.property_collections = {}
    self.file_collections = {}
    self.ttl_check_list = {}
    self.emit_signal_cb = nil
end

local function check_property_ttl(collection, current_time, ttl)
    if not collection then
        return
    end

    for _, data in pairs(collection) do
        for _, props in pairs(data) do
            for _, item in pairs(props) do
                if (current_time - item.update_tick) > ttl then
                    item.expired = true
                else
                    item.expired = false
                end
            end
        end
    end
end

local function start_check_ttl_task(collections, ttl_check_list)
    local current_time
    skynet.fork_once(function()
        while true do
            skynet.sleep(consts.SKYNET_ONE_SECOND)
            current_time = vos.vos_tick_get()
            for src_name, ttl in pairs(ttl_check_list) do
                check_property_ttl(collections[src_name], current_time, ttl)
            end
        end
    end)
end

function data_mgmt:register()
    if data_sync_instance:get_instance().communication_type == consts.COMMUNICATION_TYPE_CAN then
        canbus_dispatcher.get_instance():register_cmd_handler(definition.can_cmds.PROPERTY_SYNC_REQ, function(...)
            self:can_recv(...)
        end)
    else
        receiver_instance.get_instance():register_callback(function(...)
            self:tcp_propery_recv(...)
        end, function (...)
            self:tcp_file_recv(...)
        end)
    end
    start_check_ttl_task(self.property_collections, self.ttl_check_list)
end

-- 更新本地数据
local function update_datas(collection, update_name, data, src_id, emit_signal_cb)
    local cur_tick = vos.vos_tick_get()
    for interface, props in pairs(data) do
        if not collection[interface] then
            collection[interface] = {}
        end
        for prop, value in pairs(props) do
            if not collection[interface][prop] then
                collection[interface][prop] = {expired = false, value = value}
            else
                collection[interface][prop].value = value
            end
            collection[interface][prop].update_tick = cur_tick
        end
        -- 不关注属性是否变化，只要有同步新数据就发送信号
        if emit_signal_cb then
            emit_signal_cb(src_id, update_name, interface)
        end
    end
end

function data_mgmt:process_recv_data(src_id, msgs)
    local datas
    if type(datas) == 'table' then
        -- 先格式化数据
        local ok, res = pcall(json.decode, msgs)
        if not ok then
            log:error('format sync property failed, %s', res)
            return
        end
        datas = res
    else
        datas = msgs
    end
    local src_name = datas.SrcName
    local update_name = datas.UpdateName
    local data = datas.Data

    -- 匹配property_sync对象
    local sync_object = object_mgmt.get_instance().property_sync_list[src_name]
    if not sync_object then
        log:error('find sync object [%s] failed', src_name)
        return
    end

    if not self.property_collections[update_name] then
        self.property_collections[update_name] = {}
    end

    if not self.property_collections[update_name][src_id] then
        self.property_collections[update_name][src_id] = {}
        if sync_object.ttl ~= 0 then  -- 0表示不做限制
            self.ttl_check_list[update_name] = sync_object.ttl * 1000  -- 单位1000(ms)
        end
    end

    update_datas(self.property_collections[update_name][src_id], update_name, data, src_id, self.emit_signal_cb)
end

function data_mgmt:can_recv(can_id, channel, msgs)
    local src_id = can_id.src_id
    if src_id == canbus_collection:get_instance().identifier then
        return
    end
    local payload = definition.sync_property_payload_pattern:unpack(table.concat(msgs))

    self:process_recv_data(src_id, payload.datas)
end

function data_mgmt:tcp_propery_recv(msgs)
    self:process_recv_data(msgs['Identifier'], msgs)
end

function data_mgmt:tcp_file_recv(msg)
    -- 远端需要有对应的配置对象
    local src_name = msg['SrcName']
    local sync_object = object_mgmt.get_instance().file_sync_list[src_name]
    if not sync_object then
        log:error('find sync object [%s] failed', src_name)
        return
    end

    local identifier = msg['Identifier']
    -- 如果是头文件则直接清空缓存
    if msg['Type'] == 'Head' then
        self.file_collections[identifier] = {}
        self.file_collections[identifier].Head = msg.Data
        return
    end
    -- 如果不是头文件而且没有缓存数据说明发生了复位等导致数据不连续，不再处理
    if not self.file_collections[identifier] then
        return
    end
    -- 是否是首次接收数据
    if not self.file_collections[identifier].Data then
        self.file_collections[identifier].Data = ''
    end

    self.file_collections[identifier].Data = self.file_collections[identifier].Data .. msg.Data
    -- 如果没传输结束则继续等待接收
    if not msg['End'] then
        return
    end
    -- 已经传输完成则进行格式化
    local ok, err = pcall(function ()
        self.file_collections[identifier].Data = json.decode(self.file_collections[identifier].Data)
    end)
    if not ok then
        log:info('format sync file failed, %s', err)
        self.file_collections[identifier] = {}
        return
    end
    -- 开始处理同步的数据
    self:process_sync_data(identifier, sync_object.sync_type)
end

local function process_file(self, path, data, identifier, sync_type)
    -- 统一在/dev/shm路径创建临时文件
    local tmp_dir = string.format('%s/%s', tmp_dir_path, identifier)
    utils.mkdir_with_parents(tmp_dir, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP | utils.IROTH)
    local head = self.file_collections[identifier].Head[path]
    local ok, err, file, file_err
    local tmp_path = string.format('%s/%s', tmp_dir, head.name)
    ok, err = pcall(function ()
        file, file_err = file_sec.open_s(tmp_path, 'wb')
        if not file then
            log:info('can not open file [%s], %s', tmp_path, file_err)
        end
        file:write(data)
        file:close()
    end)
    if not ok then
        log:info('write file [%s] error [%s]', tmp_path, err)
    end
    if sync_type == consts.DATASYNC_FILE_TYPE_AUTO then
        utils.copy_file(tmp_path, head.path)
        utils.chmod(head.path, head.permissions)
    elseif sync_type == consts.DATASYNC_FILE_TYPE_SEND then
        -- 修改权限后由订阅方自行处理目标文件
        utils.chmod(tmp_path, head.permissions)
    end
end

local function process_file_link(self, path, data, identifier, sync_type)
    if sync_type == consts.DATASYNC_FILE_TYPE_AUTO then
        local command = string.format('ln -s "%s" "%s"', data, path)
        local handle = io.popen(command)
        local exit_code = handle:close()
        if exit_code ~= 0 then
            log:info('create link [%s] to [%s] failed', path, data)
        end
    elseif sync_type == consts.DATASYNC_FILE_TYPE_SEND then  -- 信号通知链接文件对应的目标文件路径
    end
end

--  如果不立刻覆盖，则目录结构应该放置在缓存目录下
local function create_dir(info, catch_path)
    if catch_path then
        utils.mkdir_with_parents(catch_path .. info.path, info.permissions)
    else
        utils.mkdir_with_parents(info.path, info.permissions)
    end
    for _, subdir in pairs(info.subdirs) do
        create_dir(subdir, catch_path)
    end
end

-- 通知模式下需要在临时目录(例如/dev/shm/bmc_datasync/1)下创建
-- 覆盖模式下直接删除原目录
local function process_dir(self, path, data, identifier, sync_type)
    -- 直接覆盖模式需要删除原路径
    if sync_type == consts.DATASYNC_FILE_TYPE_AUTO then
        utils.remove_file(data.path)
        create_dir(data)
    elseif sync_type == consts.DATASYNC_FILE_TYPE_SEND then
        local tmp_path = tmp_dir_path .. '/' .. identifier
        utils.remove_file(tmp_path .. data.path)
        create_dir(data, tmp_path)
    end
end

local process_list = {
    ['File'] = process_file,
    ['Dir'] = process_dir,
    ['File_Link'] = process_file_link
}

function data_mgmt:process_sync_data(identifier, sync_type)
    local file_type
    for path, data in pairs(self.file_collections[identifier].Data) do
        file_type = self.file_collections[identifier].Head[path].type
        process_list[file_type](self, path, data, identifier, sync_type)
    end
    -- 每同步完一次清空内存数据
    self.file_collections[identifier] = {}
    -- 覆盖模式下需要删除缓存目录
    if sync_type == consts.DATASYNC_FILE_TYPE_AUTO then
        utils.remove_file(tmp_dir_path .. '/' .. identifier)
    end
end

local function format_sync_properties(collection, interface, res, src_id)
    if not collection[interface] then
        return
    end

    for prop, item in pairs(collection[interface]) do
        table.insert(res, {
            Identifier = src_id,
            PropertyDetails = {
                {
                    PropertyName = prop,
                    PropertyItems = {
                        PropertyValue = type(item.value) == 'table' and json.encode(item.value) or tostring(item.value),
                        Expired = tostring(item.expired)
                    }
                }
            }
        })
    end
end

function data_mgmt:get_sync_property(identifier, object_name, interface)
    local res = {}
    if not self.property_collections[object_name] then
        log:info('sync property [%s][%s][%s] not exist', identifier, object_name, interface)
        return res
    end

    for src_id, collection in pairs(self.property_collections[object_name]) do
        if identifier == 255 or identifier == src_id then -- 255表征获取所有
            format_sync_properties(collection, interface, res, src_id)
        end
    end
    return res
end

local function format_dump_properties(collection, content, src_id, object_name, format)
    local items = {}

    for interface, props in pairs(collection) do
        for prop, item in pairs(props) do
            table.insert(items, string.format(format[1], object_name))
            table.insert(items, string.format(format[2], src_id))
            table.insert(items, string.format(format[3], interface))
            table.insert(items, string.format(format[4], prop))
            table.insert(items, string.format(format[5], type(item.value) == 'table' and
                json.encode(item.value) or tostring(item.value)))
            table.insert(items, string.format(format[6], tostring(item.expired)))
            table.insert(content, table.concat(items, '|'))
            items = {}
        end
    end
end

function data_mgmt:dump_sync_datas(content)
    table.insert(content, 'SyncDatas:')

    local items = {}
    local format = {'%-25s', '%-10s', '%-30s', '%-10s', '%-10s', '%-10s'}
    local title = {'SyncObjectName', 'Identifier', 'Interface', 'Property', 'Value', 'Expired'}
    for i =  1, #format do
        table.insert(items, string.format(format[i], title[i]))
    end
    table.insert(content, table.concat(items, '|'))

    for object_name, src_collections in pairs(self.property_collections) do
        for src_id, collection in pairs(src_collections) do
            format_dump_properties(collection, content, src_id, object_name, format)
        end
    end
end

-- 格式化列表的key为字符串
local function format_props(props)
    local res = {}
    for prop, _ in pairs(props) do
        table.insert(res, prop)
    end
    return table.concat(res, ',')
end

local function dump_file(content, path, obj)
    local items = {}
    local format = {'%-20s', '%-15s', '%-15s', '%-15s'}
    local title = {'Path', 'SyncType', 'SyncTotalCount', 'SyncFailedCount'}

    for i =  1, #format do
        table.insert(items, string.format(format[i], title[i]))
    end
    table.insert(content, table.concat(items, '|'))
    items = {}

    table.insert(items, string.format(format[1], path))
    table.insert(items, string.format(format[2], obj.sync_type))
    table.insert(items, string.format(format[3], obj.sync_total_count))
    table.insert(items, string.format(format[4], obj.sync_failed_count))
    table.insert(content, table.concat(items, '|'))
    items = {}

    table.insert(content, '\nSyncFiles:')
    for i, file_path in ipairs(obj.file_list) do
        table.insert(content, string.format('%s. %s', i, file_path))
    end
end

local function dump_sender(content, tcp_mgmt)
    table.insert(content, 'Cureent Sync Role: Sender:\n')
    table.insert(content, 'Sender:\n')
    local items = {}
    local format = {'%-15s', '%-30s'}
    local title = {'Identifier', 'Address'}

    for i =  1, #format do
        table.insert(items, string.format(format[i], title[i]))
    end
    table.insert(content, table.concat(items, '|'))
    items = {}

    table.insert(items, string.format(format[1], tcp_mgmt.identifier))
    table.insert(items, string.format(format[2], string.format('%s:%s', tcp_mgmt.address, tcp_mgmt.port)))
    table.insert(content, table.concat(items, '|'))
    items = {}

    table.insert(content, '\nReceiver:')
    format = {'%-25s', '%-15s'}
    title = {'Address', 'ConnectStatus'}
    for i =  1, #format do
        table.insert(items, string.format(format[i], title[i]))
    end
    table.insert(content, table.concat(items, '|'))
    items = {}

    local sender = sender_instance:get_instance()
    for path, addr in pairs(sender.destination_addr_list) do
        table.insert(items, string.format(format[1], addr:match('(%d+%.%d+%.%d+%.%d+:%d+)')))
        table.insert(items, string.format(format[2], sender.cnc_client_list[path] and true or false))
    end
    table.insert(content, table.concat(items, '|'))
    items = {}

    table.insert(content, '\nFileSyncInfo:')
    for path, obj in pairs(object_mgmt.get_instance().file_sync_list) do
        dump_file(content, path, obj)
    end
end

local function dump_recevier(content, tcp_mgmt)
    table.insert(content, 'Cureent Sync Role: Receiver:\n')
    table.insert(content, 'Receiver:\n')
    local items = {}
    local format = {'%-15s', '%-30s'}
    local title = {'Identifier', 'Address'}

    for i =  1, #format do
        table.insert(items, string.format(format[i], title[i]))
    end
    table.insert(content, table.concat(items, '|'))
    items = {}

    table.insert(items, string.format(format[1], tcp_mgmt.identifier))
    table.insert(items, string.format(format[2], string.format('%s:%s', tcp_mgmt.address, tcp_mgmt.port)))
    table.insert(content, table.concat(items, '|'))
end

function data_mgmt:dump_tcp(content)
    if data_sync_instance:get_instance().communication_type ~= consts.COMMUNICATION_TYPE_TCP then
        table.insert(content, 'Cureent Communication Type Not TCP \n')
        return
    end

    local tcp_mgmt = tcp_mgmt_instance:get_instance()
    if tcp_mgmt.sync_role == consts.DATASYNC_ROLE_SENDER then
        dump_sender(content, tcp_mgmt)
    else
        dump_recevier(content, tcp_mgmt)
    end
end

function data_mgmt:dump(path)
    -- dump content into file
    local fd = file_checker.open_s(path .. '/bmc_datasync.txt', 'w')
    if not fd then
        log:error('dump property sync info failed, because file open failed')
        return
    end

    local content = {}
    self:dump_tcp(content)
    table.insert(content, '\nSyncObjectInfo:')
    local items, format, title
    for name, obj in pairs(object_mgmt.get_instance().property_sync_list) do
        -- makeup the title
        items = {}
        format = {'%-25s', '%-25s', '%-10s', '%-10s', '%-10s'}
        title = {'SyncObjectName', 'PropertySyncName', 'SyncMode', 'SyncTotalCount', 'SyncFailedCount'}
        for i =  1, #format do
            table.insert(items, string.format(format[i], title[i]))
        end
        table.insert(content, table.concat(items, '|'))

        -- makeup the content
        items = {}
        table.insert(items, string.format(format[1], name))
        table.insert(items, string.format(format[2], obj.sync_object_name or 'null'))
        table.insert(items, string.format(format[3], obj.sync_mode))
        table.insert(items, string.format(format[4], obj.sync_total_count))
        table.insert(items, string.format(format[5], obj.sync_failed_count))
        table.insert(content, table.concat(items, '|'))

        -- makeup the title
        items = {}
        format = {'%-10s', '%-40s'}
        title = {'\nSyncInterface', 'SyncProperty'}
        for i =  1, #format do
            table.insert(items, string.format(format[i], title[i]))
        end
        table.insert(content, table.concat(items, '|'))

        -- makeup the content
        for interface, props in pairs(obj.intf_to_prop) do
            items = {}
            table.insert(items, string.format(format[1], interface))
            table.insert(items, string.format(format[2], format_props(props)))
            table.insert(content, table.concat(items, '|'))
        end
        table.insert(content, '')
    end

    self:dump_sync_datas(content)

    utils.safe_close_file(fd, fd.write, fd, table.concat(content, '\n'))
    utils.chmod(path .. '/bmc_datasync.txt', utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
end

return singleton(data_mgmt)